Milestone 1
10 Oct 2023Section 1 : Acquisition de données
Question 1.1
Les données que nous désirons acquérir sont disponibles à partir d’un API utilisant des url telles que :
https://statsapi.web.nhl.com/api/v1/game/2017020001/feed/live/
où 2017020001 correspond au game_id de la partie. Le game_id est composé de trois sections, soit l’année de la saison (2017), le type de saison (02) et le numéro de la partie (0001). Les types de saison nous intéressant sont 02 pour les saisons régulières. Pour obtenir l’entièreté de la saison régulière de 2016, on peut simplement itérer sur le numéro de la partie en utilisant :
access_point = "https://statsapi.web.nhl.com/api/v1/game/"
season = 2016
game_id = 1
game_id_str = f"{season}02{str(game_id).zfill(4)}"
url = f"{access_point}{game_id_str}/feed/live/"
et augmenter game_id. On peut alors définir le nom du fichier où les données seront sauvegardées afin de vérifier s’il existe déjà localement et ne pas le télécharger.
save_directory = "../NHLGameData"
filename = os.path.join(save_directory, f"game_{game_id_str}.json")
if os.path.exists(filename):
#do not download filename and continue to next file
game_id += 1
continue
Dans le cas ᴏù nous voulons télécharger le fichier, il nous faudra commencer par vérifier si celui-ci existe dans l’API.
response = requests.get(url)
if response.status_code == 200:
# download file
elif response.status_code == 404:
# Game not found, exit the loop
break
else:
print(f"Error in regular match ID {game_id_str}: {response.status_code}")
Si la partie existe et est accessible, le code renvoyé sera 200. Si la partie n’existe pas (erreur 404), cela veut dire que le game_id présent est devenu trop haut et il est nécessaire de quitter la boucle d’itération de match afin de ne pas faire des demandes inutiles à l’API. Si un autre code est renvoyé, cela n’est pas normal et il est intéressant de le notifier dans l’interface. Finalement, si on veut télécharger le fichier, il est nécessaire de savoir que l’API enregistre ses données en format json. On peut donc accéder aux données pertinentes et les téléchargées dans un fichier.
playByPlay = response.json()['liveData']['plays']['allPlays']
home_team = responses.json()['gameData']['teams']['home']['name']
away_team = response.json()['gameData']['teams']['away']['name']
periods = response.json()['liveData']['linescore']['periods']
# create and save the json informations into the file
with open(filename, "w") as file:
data_to_save = {"playByPlay": playByPlay,"periods": periods, "home": home_team, "away": away_team, "gameID": game_id_str}
json.dump(data_to_save, file)
Section 2 : Débogage interactif
Question 2.1

L’outil développé permet de visualiser les différents évènements assez facilement à l’aide de ipywidget tout en naviguant les différentes parties, années et type de saisons. Sur une erreur de la visualisation, par exemple sur un début de partie ou n’importe quel évènement où les coordonnées ne sont pas disponibles, le texte en format json est montré à la place. L’outil balaye les fichiers locaux disponibles localement pour faire la liste des données à visualiser.

import ipywidgets as widgets
from IPython.display import display
import matplotlib.image as mpimg
import json
import os
import matplotlib.pyplot as plt
class event_visualizer(object):
def __init__(self, path, img_path):
self.path = path
self.rink_img = mpimg.imread(img_path)
self.initialize_values()
self.create_widgets()
def initialize_values(self):
self.season_type = '02' # init only
self.games_list = self.get_games_list()
self.year_list = self.get_year_list()
self.year = self.year_list[0] # init only
self.game_ids = self.get_games_in_season()
self.game_id = self.game_ids[0] # init only
self.file = self.read_file()
self.events = self.file['playByPlay']
self.event_id = 0 # init only
def create_widgets(self):
self.w_season_type = widgets.ToggleButtons(
options=[('Regular', '02'), ('Playoff', '03')],
description='Season type'
)
self.w_season_year = widgets.ToggleButtons(
options=self.year_list,
description="Season year"
)
self.w_game_ids = widgets.SelectionSlider(
options=self.game_ids,
value=self.game_ids[0],
description="Game id"
)
self.w_season_year.observe(self.update, 'value')
self.w_season_type.observe(self.update, 'value')
self.w_game_ids.observe(self.update, 'value')
self.grid = widgets.GridspecLayout(3,1)
self.grid[0,0] = self.w_season_type
self.grid[1,0] = self.w_season_year
self.grid[2,0] = self.w_game_ids
def update(self, *args):
self.season_type = self.w_season_type.value
self.games_list = self.get_games_list()
self.year_list = self.get_year_list()
self.w_season_year.options = self.year_list
self.year = self.w_season_year.value
self.game_ids = self.get_games_in_season()
self.w_game_ids.options = self.game_ids
self.game_id = self.w_game_ids.value
self.file = self.read_file()
self.events = self.file['playByPlay']
def get_games_list(self):
games_list = os.listdir(self.path)
return [ i[5:9] + "_" + i[11:15] for i in games_list if i[9:11]==self.season_type]
def get_year_list(self):
year_list = list(set( [i.split("_")[0] for i in self.games_list] ))
year_list.sort()
return year_list
def get_games_in_season(self):
season_games = [ i.split("_")[1] for i in self.games_list if i[0:4]==self.year ]
season_games.sort() #should be by default but still safer
return season_games
def read_file(self):
f = open(self.path + "game_{year}{season}{id}.json".format(year=self.year,
season=self.season_type,
id=self.game_id)
)
data = json.load(f)
return data
def plot_event(self, event_id):
try :
x = self.events[event_id]['coordinates']['x']
y = self.events[event_id]['coordinates']['y']
title = self.events[event_id]['result']['description']
time = self.events[event_id]['about']['periodTime']
period = self.events[event_id]['about']['period']
fig = plt.figure()
ax = fig.add_axes([0,0,1,1])
ax.imshow(self.rink_img, extent=[100, -100, 42.5, -42.5])
ax.set_xlim(-100, 100)
ax.set_xlabel("feet")
ax.set_ylim(-42.5, 42.5)
ax.set_ylabel("feet")
ax.set_title("{description}\n{time} P-{period}\n".format(description=title,
time=time,
period=period)
)
ax.plot(x,y, 'ok')
plt.figtext(0.15, 0.8, self.file['home']) # positions for these are hard coded since they shouldn't change
plt.figtext(0.65, 0.8, self.file['away'])
plt.show()
except:
print( json.dumps(self.events[event_id], indent=4) )
game_viz = event_visualizer("data/", "nhl_rink.png")
display(game_viz.grid)
widgets.interact(game_viz.plot_event, event_id=(0, len(game_viz.events)-1))
Section 4 : Nettoyer les données
Question 4.1
df.sample(10)
Question 4.2
Dans un match de hockey, chaque action sur la glace a son importance, qu’il s’agisse d’un tir, d’un but ou même d’une pénalité. Lorsque nous examinons les statistiques, nous pouvons souvent voir le nombre de tirs ou de buts, mais une dimension souvent négligée est la “force réelle” lors de ces actions. La force réelle se réfère à la situation numérique sur la glace, comme 5 contre 4 lors d’une pénalité ou 5 contre 5 en jeu égal.
Pour déterminer cette force réelle, il est essentiel d’intégrer les données des événements de pénalité dans notre DataFrame. Chaque pénalité indique la durée pendant laquelle une équipe jouera en infériorité numérique. En suivant ces pénalités et en les associant aux tirs et aux buts qui se produisent pendant leur durée, nous pouvons déduire la force réelle de chaque attaque. Par exemple, si une pénalité de deux minutes est infligée à 10:00 et qu’un tir est enregistré à 10:30, on peut conclure que ce tir a eu lieu en avantage numérique.
Mais attention, d’autres événements peuvent influencer la situation. Un but marqué pendant un avantage numérique pourrait annuler la pénalité, ou des pénalités simultanées pourraient maintenir un jeu à 5 contre 5. Il est donc crucial de prendre en compte tous les événements du jeu.
Question 4.3
Efficacité des Joueurs
En utilisant les données sur les joueurs qui tirent, on pourrait créer une métrique pour évaluer l’efficacité des tirs de chaque joueur. Cela consisterait à calculer le pourcentage de tirs d’un joueur qui se transforment en buts. Cette métrique pourrait aider à identifier les joueurs les plus dangereux ou les plus efficaces en face du but.
Efficacité du Gardien de but
En se basant sur le nombre de tirs qu’un gardien de but reçoit et le nombre de buts qu’il concède, on pourrait calculer un taux d’arrêt pour chaque gardien. Cela donnerait une mesure directe de l’efficacité d’un gardien à stopper les tirs adverses.
Tirs en Contre-Attaque - Rebonds
Pour identifier un tir en contre-attaque, on pourrait examiner la séquence des événements. Si un “Hit” ou un “Turnover” est suivi rapidement par un tir de l’équipe opposée, cela pourrait indiquer un tir en contre-attaque. Si un tir est suivi immédiatement par un autre tir de la même équipe sans qu’un événement significatif se produise entre les deux (comme un dégagement ou un arrêt de jeu), le second tir pourrait être considéré comme un rebond.
Section 5 : Visualisations simples
Question 5.1
Le graphique ci-dessus présente une comparaison des différents types de tirs par rapport aux buts marqués pour chaque type lors de la saison 2019-20.
Type de tir le plus courant
On peut constater que “Wrist Shot” est clairement le type de tir le plus fréquemment utilisé par les équipes.
Type de tir le plus dangereux
En observant la proportion de buts par rapport au nombre total de tirs, le “Tip-In” semble être le plus efficace pour marquer des buts. Cependant, le “Wrist Shot”, en raison de sa fréquence élevée, a également produit un grand nombre de buts.
Pourquoi ce type de graphique
On a choisi un graphique à barres empilées car il permet de comparer directement le nombre total de tirs et le nombre de buts pour chaque type de tir. Les couleurs Rouges et Vertes permettent de distinguer les buts des tirs.
Question 5.2
Comprendre la relation entre la distance du tir et la probabilité de marquer est crucial pour les équipes et les entraîneurs. Cette connaissance peut influencer les stratégies offensives des équipes par exemple. Il est donc toujours utile de visualiser ces relations. Les graphiques ci-dessous montrent la probabilité de marquer un but en fonction de la distance du tir pour les saisons 2018-19, 2019-20 et 2020-21.
Observations
Au cours des saisons 2018 et 2019, les données montrent clairement que les tirs à une distance plus courte ont une probabilité significativement plus élevée de se transformer en buts. Cependant, la saison 2020-21 présente une tendance différente, avec des probabilités de marquer qui ne sont pas aussi élevées, même pour les tirs à courte distance. Cette différence est attribuée au nombre de matches joués cette saison en raison de la pandémie de COVID-19 À mesure que la distance augmente, la probabilité de marquer diminue. Cependant, il y a certaines distances où il y a des pics ou des augmentations notables de la probabilité. Cela pourrait être dû à des tirs précis, comme les « Slap shots », qui sont puissants et peuvent surprendre le gardien. Il y a une certaine cohérence dans la tendance à travers les trois saisons, bien que de légères variations puissent être observées.
Pourquoi ce type de graphique
On a choisi un graphique linéaire car il permet de voir clairement comment une variable (dans ce cas, la probabilité de marquer) change en fonction d’une autre variable continue (dans ce cas, la distance du tir). Les graphiques linéaires sont un excellent choix pour notre situation puisqu’ils captent les pics et les creux.
Question 5.3
Tirs rapprochés
Comme attendu, il y a une concentration plus élevée (zones plus claires) de buts pour les tirs effectués à courte distance, en particulier pour les “Tip-Ins” et les “Snap Shots”. Cela renforce l’idée que les tirs rapprochés ont généralement une probabilité plus élevée de se transformer en des buts.
Efficacité des “Tip-Ins”
Les “Tip-Ins”, semblent être particulièrement efficaces à courte distance. Cela pourrait être dû à la nature imprévisible de ces tirs, rendant difficile pour le gardien de but de les arrêter.
Variabilité avec la distance
Pour les tirs tels que les “Slap Shots” et les “Wrist Shots”, il y a une variabilité notable dans le pourcentage de buts à mesure que la distance augmente. Bien que ces tirs soient généralement moins efficaces à longue distance, il y a des zones où ils ont un taux de réussite relativement élevé, peut-être en raison de la puissance ou de la précision du tir.
Zones plus foncées à longue distance
Comme prévu, la plupart des types de tirs ont un pourcentage de buts plus faible lorsqu’ils sont pris à longue distance. Cela est logique car il est généralement plus difficile de marquer depuis une distance éloignée. Sauf que pour quelques cas, on peut remarquer des grands pourcentages et c’est a cause du nombre limités des tirs effectués à ces distances durant cette saison.
Section 6 : Visualisations avancées
Question 6.1
Question 6.2
La shot map est un outil efficace pour identifier les équipes qui ont réussi au cours d’une saison donnée. D’une manière générale, ces diagrammes permettent d’identifier les zones de la glace offensive dans lesquelles une équipe est déficiente ou forte en matière de tirs. D’une manière générale, il est possible d’interpréter les équipes qui se sont distinguées au cours d’une saison donnée. Les équipes qui ont un plus grand nombre de zones rouges ont une plus grande probabilité de gagner des matchs, statistiquement parlant, étant donné qu’elles font plus de tentatives que la moyenne.
Question 6.3
Colorado Avalanche au cours de la saison 2016-17 a affiché de petites zones où ils ont tiré plus que la ligue (minimalement), plutôt, la plupart de la glace offensive est couverte en bleu. Ces données peuvent indiquer que l’Avalanche n’a pas pris beaucoup de tirs au cours de cette saison, ce qui diminue ses chances de réussite. En revanche, les chiffres des tirs de la saison 2020-21 sont bien meilleurs, avec moins de zones bleues et plus de zones où les tirs sont supérieurs à ceux de la ligue. Notamment, à partir d’une distance de 10 pieds du côté droit du filet, ils ont obtenu de très bons résultats par rapport à la ligue. En augmentant leur nombre de tirs dans une zone où le score est si élevé, ils sont en bonne position pour réaliser une bonne saison.
Compte tenu de toutes ces données et informations, il n’est pas surprenant d’apprendre que l’Avalanche du Colorado a terminé premier de sa division en 2020-21 tout en accumulant plus de points au total que n’importe quelle autre équipe de la ligue. À l’inverse, ils ont été de loin l’équipe la moins performante de la ligue lors de la saison 2016-17, récoltant 21 points de moins que l’équipe la deuxième moins bien placée.
Question 6.4
Comme dans l’étude sur l’Avalanche du Colorado, un tracé bleu foncé permet d’expliquer la faiblesse des Sabres de Buffalo, tandis que des tracés moins bleus et plus rouges pour le Lightning de Tamba Bay expliquent leur succès. Sur les trois saisons de 2018 à 2020, les Sabres affichent une large zone où ils tirent moins que la moyenne du côté droit de la glace offensive et n’affichent aucune zone où ils ont des chiffres de tir supérieurs à ceux de la ligue. Le Lightning, en revanche, présente moins de zones bleues et plus de zones rouges au cours de ces trois années, notamment lors de la saison 2020-21 où il tire plus que la ligue sur une grande partie du milieu de la patinoire offensive. Étant donné que Tamba Bay a remporté la Coupe Stanley lors des saisons 2019-20 et 2020-21, il n’est pas surprenant de voir ce type de tracés.
Les cartes des tirs sont assez descriptives étant donné que, statistiquement parlant, les équipes qui attaquent plus et tirent plus que la moyenne marqueront plus de buts et gagneront plus de matchs. Cependant, comme pour la plupart des statistiques, tout doit être pris avec un grain de sel. Le hockey est un sport complexe et de nombreux facteurs définissent une équipe performante. Par exemple, un bon gardien de but peut augmenter considérablement la force d’une équipe. Si une équipe a des shot maps qui ne sont pas très attrayantes (beaucoup de bleu et peu de rouge), cela ne suffit pas pour conclure que cette équipe s’est mal comportée au cours d’une saison donnée. Même si elle est faible sur le plan offensif, une équipe peut très bien gagner des matchs en étant forte sur le plan défensif et en ne concédant pas beaucoup de buts (un gardien de but fort aide dans ce domaine).